Skip to content

feat: Add Azure CLI auth source - #265

Open
shirasassoon wants to merge 41 commits into
microsoft:mainfrom
shirasassoon:feature/azure-cli-auth-poc
Open

feat: Add Azure CLI auth source#265
shirasassoon wants to merge 41 commits into
microsoft:mainfrom
shirasassoon:feature/azure-cli-auth-poc

Conversation

@shirasassoon

@shirasassoon shirasassoon commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Add azure_cli as a new identity type that delegates token acquisition to Azure CLI via azure-identity's AzureCliCredential. This allows tools calling fab to reuse an existing az login session instead of requiring a separate interactive fab auth login.

Changes

Core auth (src/fabric_cli/core/fab_auth.py)

  • Add azure_cli to AUTH_KEYS identity type allowlist
  • Add _acquire_token_from_azure_cli() using AzureCliCredential
  • Per-command tenant drift check — compares stored tenant against current Azure CLI session via az account show
  • In-memory token cache with 60-second refresh buffer to reduce subprocess overhead
  • Tenant cache (10s TTL) to avoid repeated az account show calls during multi-scope operations
  • set_azure_cli() clears token cache on every login to prevent cross-tenant stale tokens
  • shutil.which("az") for Windows compatibility (az.cmd resolution)

Command handler (src/fabric_cli/commands/auth/fab_auth.py)

  • Add --azure-cli flag to fab auth login
  • Add "Azure CLI authentication" option to interactive login menu
  • Show auth_source in fab auth status output

Error handling (src/fabric_cli/errors/auth.py)

  • Centralized error messages for all Azure CLI auth scenarios
  • SDK exception allowlist: ClientAuthenticationError, HttpResponseError, ServiceRequestError, ServiceResponseError surfaced verbatim (pre-sanitized by azure-identity SDK)
  • All other exceptions receive a generic safe message — no token content is ever leaked

Documentation

  • docs/commands/auth/index.md--azure-cli flag in command reference with separate usage blocks per auth method
  • docs/examples/auth_examples.md — Azure CLI auth examples with tenant behavior note

Dependencies

  • Add azure-identity>=1.15.0

Tests

43 automated tests across 4 files:

File Count Coverage
test_fab_auth_azure_cli.py 34 Core auth: token acquisition, tenant drift, caching, scope handling, cache invalidation, tenant discovery, auth method isolation
test_fab_auth_parser.py 4 Parser: --azure-cli flag mapping
test_fab_msal_bridge_azure_cli.py 2 Bridge: MsalTokenCredential dispatch with azure_cli identity
test_fab_auth_command_azure_cli.py 3 Command: login/status/logout end-to-end

Key test scenarios:

  • Auth method isolation — bidirectional: user/SPN don't invoke AzureCliCredential, azure_cli doesn't invoke MSAL
  • Tenant drift detection and mismatch error
  • Token cache expiry and refresh buffer
  • Cross-tenant stale token cache clearing on login
  • Windows az.cmd resolution via shutil.which
  • Azure CLI not installed / not logged in

Security

  • Error messages sanitized via SDK exception allowlist (not denylist)
  • Auth methods are mutually exclusive — no credential path overlap
  • Per-command tenant drift check prevents wrong-tenant operations
  • Token cache cleared on every login to prevent cross-tenant reuse
  • shutil.which("az") resolves absolute path before subprocess (matches azure-identity SDK pattern)
  • shell=False for all subprocess calls — no injection risk

Open questions for team

  1. Per-command drift check vs. performance: Current behavior adds ~1s per CLI command for tenant verification. Alternative is to rely on SDK tenant pinning (no overhead) but loses the clear "tenant mismatch" error UX.
  2. az logout config persistence: When Azure CLI session is interrupted and restored with the same tenant, fab silently resumes. Should fab require explicit re-authentication instead?

Add azure_cli as a new identity type that delegates token acquisition
to Azure CLI via azure-identity's AzureCliCredential. This allows
tools calling fab to reuse an existing az login session instead of
requiring a separate interactive fab auth login.

Changes:
- Add 'azure_cli' to AUTH_KEYS identity type allow-list
- Add _acquire_token_from_azure_cli() using AzureCliCredential
- Add --azure-cli flag to fab auth login
- Add 'Azure CLI' option to interactive login menu
- Show auth_source in fab auth status output
- Add azure-identity>=1.15.0 dependency
- Add 12 unit tests covering dispatch, scopes, errors, sanitization

Security: error messages are sanitized to never leak token content.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings July 15, 2026 10:46
@shirasassoon
shirasassoon requested a review from a team as a code owner July 15, 2026 10:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new authentication source (azure_cli) to Fabric CLI, delegating token acquisition to Azure CLI (via azure-identity’s AzureCliCredential) so fab can reuse an existing az login session. It also wires the new auth source into fab auth login (flag + interactive option) and exposes the selected auth source in fab auth status.

Changes:

  • Add azure_cli as an allowed identity type and implement Azure CLI token acquisition in FabAuth.
  • Add --azure-cli flag plus an “Azure CLI” interactive login option; include auth_source in auth status output.
  • Add azure-identity>=1.15.0 dependency and a new unit test module for Azure CLI auth.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/test_core/test_fab_auth_azure_cli.py Adds unit tests for the new Azure CLI auth flow, scopes, and error sanitization.
src/fabric_cli/parsers/fab_auth_parser.py Adds --azure-cli to fab auth login and updates examples.
src/fabric_cli/core/fab_constant.py Extends identity type allow-list to include azure_cli.
src/fabric_cli/core/fab_auth.py Implements set_azure_cli() and _acquire_token_from_azure_cli() and dispatches in acquire_token().
src/fabric_cli/commands/auth/fab_auth.py Wires Azure CLI auth into login flows and adds auth_source to status output.
pyproject.toml Adds the azure-identity runtime dependency required for AzureCliCredential.

Comment thread src/fabric_cli/core/fab_auth.py Outdated
Comment thread src/fabric_cli/core/fab_auth.py Outdated
Comment thread src/fabric_cli/commands/auth/fab_auth.py
Comment thread tests/test_core/test_fab_auth_azure_cli.py Outdated
Comment thread tests/test_core/test_fab_auth_azure_cli.py Outdated
Copilot AI review requested due to automatic review settings August 11, 2026 08:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (6)

tests/test_core/test_fab_auth_azure_cli.py:36

  • These fixtures are unused in this test module, and the singleton-reset logic inside them is brittle (it relies on non-existent __wrapped__ and decorator internals). After resetting the FabAuth singleton in the autouse fixture, these can be removed to keep the tests easier to maintain.
@pytest.fixture
def auth_instance(temp_dir_fixture):
    """Get a fresh FabAuth instance."""
    # Clear singleton for test isolation
    FabAuth.__wrapped__ = None  # type: ignore
    from fabric_cli.core import fab_auth as fab_auth_module

    if FabAuth in fab_auth_module.singleton.__wrapped__:  # type: ignore
        del fab_auth_module.singleton.__wrapped__[FabAuth]  # type: ignore
    return FabAuth()

tests/test_core/test_fab_auth_azure_cli.py:56

  • This fixture is unused, and it attempts to monkeypatch FabAuth.__init__.__globals__, which is not a safe or reliable way to reset the singleton (and may raise if executed). With the singleton reset handled in the autouse fixture, this block can be deleted.
@pytest.fixture
def fresh_auth(temp_dir_fixture, monkeypatch):
    """Get a fresh FabAuth instance with singleton cleared."""
    # Reset singleton instances dict
    import fabric_cli.core.fab_auth as auth_module

    # Access the closure variable of the singleton decorator
    singleton_instances = auth_module.singleton.__code__.co_consts  # noqa
    # Simpler approach: just patch the module-level reference
    monkeypatch.setattr(
        "fabric_cli.core.fab_auth.FabAuth.__init__.__globals__",
        {},
        raising=False,
    )
    # Re-instantiate
    auth = FabAuth.__new__(FabAuth)
    auth.__init__()
    return auth

src/fabric_cli/core/fab_auth.py:444

  • This line exceeds the repo’s Black line-length (88) and will be reformatted by CI (tox.toml:69-70). Please wrap the conditional instantiation so the formatted output is stable and easier to read.
        tenant_id = self.get_tenant_id()
        try:
            credential = AzureCliCredential(tenant_id=tenant_id) if tenant_id else AzureCliCredential()
            # AzureCliCredential.get_token expects scopes as positional args
            azure_token = credential.get_token(scope[0])

src/fabric_cli/core/fab_auth.py:461

  • This sanitized error message is long enough to violate Black’s 88-char line length and will be reformatted by CI (tox.toml:69-70). Splitting it across adjacent string literals keeps formatting stable.
            error_msg = str(e)
            if "accessToken" in error_msg or "token" in error_msg.lower():
                error_msg = "Azure CLI token acquisition failed. Run 'az account get-access-token' manually to diagnose."
            raise FabricCLIError(

src/fabric_cli/commands/auth/fab_auth.py:37

  • When --azure-cli is provided, other credential flags (e.g., -u/-p, --certificate, --federated-token, --identity) are silently ignored due to branch precedence. This can lead to confusing CLI behavior; please validate and fail fast on incompatible combinations.
    if getattr(args, "azure_cli", False):
        FabAuth().set_access_mode("azure_cli", args.tenant)
        FabAuth().set_azure_cli(args.tenant)
        FabAuth().get_access_token(scope=fab_constant.SCOPE_FABRIC_DEFAULT)
        FabAuth().get_access_token(scope=fab_constant.SCOPE_ONELAKE_DEFAULT)

tests/test_core/test_fab_auth_azure_cli.py:24

  • FabAuth is a singleton (see @singleton in fabric_cli.core.fab_auth). These tests patch config_location() per-test, but without clearing the singleton, FabAuth() will reuse the first instance (and its first auth/cache paths), causing state leakage across tests and making the tmp_path isolation ineffective.

This issue also appears in the following locations of the same file:

  • line 27
  • line 39
@pytest.fixture(autouse=True)
def temp_dir_fixture(monkeypatch, tmp_path):
    """Create a temporary directory and configure FabAuth to use it."""
    monkeypatch.setattr(
        "fabric_cli.core.fab_state_config.config_location", lambda: str(tmp_path)
    )

- Expand sanitization patterns (eyJ, Bearer, refresh_token, Authorization)
- Auto-capture tenant from az account show at login
- Tenant drift detection on every token acquisition
- In-memory token caching by audience with 60s expiry buffer
- Display tenant and auth mode at login and in auth status
- Add 11 new tests (23 total) for drift, caching, sanitization

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 11, 2026 08:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/fabric_cli/core/fab_auth.py:502

  • _acquire_token_from_azure_cli() calls credential.get_token(scope[0]) even though the method signature allows an empty scope list. If scope is empty, this will raise IndexError instead of a FabricCLIError, and it also ignores additional scopes if ever provided.
            credential = AzureCliCredential(tenant_id=stored_tenant) if stored_tenant else AzureCliCredential()
            # AzureCliCredential.get_token expects scopes as positional args
            azure_token = credential.get_token(scope[0])
            token_result = {
                "access_token": azure_token.token,

src/fabric_cli/commands/auth/fab_auth.py:35

  • --azure-cli is not validated as mutually exclusive with managed identity/service principal flags. Because the code checks azure_cli first, a user can accidentally pass conflicting flags and silently get Azure CLI auth instead of an error.
    if getattr(args, "azure_cli", False):
        FabAuth().set_access_mode("azure_cli", args.tenant)
        FabAuth().set_azure_cli(args.tenant)
        FabAuth().get_access_token(scope=fab_constant.SCOPE_FABRIC_DEFAULT)
        FabAuth().get_access_token(scope=fab_constant.SCOPE_ONELAKE_DEFAULT)

tests/test_core/test_fab_auth_azure_cli.py:18

  • FabAuth is a singleton (decorator returns a cached instance), but this autouse fixture only patches config_location/env vars and doesn’t clear the singleton cache. If any other test module instantiates FabAuth before this fixture runs, these tests will share state and potentially write auth/cache files outside tmp_path, causing order-dependent failures.
@pytest.fixture(autouse=True)
def temp_dir_fixture(monkeypatch, tmp_path):
    """Create a temporary directory and configure FabAuth to use it."""
    monkeypatch.setattr(
        "fabric_cli.core.fab_state_config.config_location", lambda: str(tmp_path)

src/fabric_cli/core/fab_auth.py:522

  • The generic exception handler includes the raw exception message in the CLI error unless it matches a small allow-list of substrings. That does not guarantee token material won’t leak (e.g., access tokens that don’t contain the current patterns), which contradicts the PR’s “never leak token content” claim.
        except Exception as e:
            # Sanitize: never include token content in error messages
            error_msg = str(e)
            if any(p.lower() in error_msg.lower() for p in self._SENSITIVE_PATTERNS):
                error_msg = "Azure CLI token acquisition failed. Run 'az account get-access-token' manually to diagnose."

Defer OneLake and Azure management token acquisition to first use,
matching the lazy approach. Tokens are cached in-memory after first call.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 11, 2026 09:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (5)

src/fabric_cli/core/fab_auth.py:477

  • Azure CLI auth introduces new FabricCLIError messages as hardcoded strings. Elsewhere in this module, auth failures use ErrorMessages.Auth.*() helpers for consistent wording and easier localization/maintenance. Consider moving these new messages into ErrorMessages.Auth and reusing them here.
        try:
            from azure.identity import AzureCliCredential, CredentialUnavailableError
        except ImportError:
            raise FabricCLIError(
                "Azure CLI auth requires the 'azure-identity' package. "
                "Install it with: pip install azure-identity",
                status_code=con.ERROR_AUTHENTICATION_FAILED,
            )

tests/test_core/test_fab_auth_azure_cli.py:36

  • These tests call FabAuth() directly, but FabAuth is a singleton (via the @singleton decorator). Without reliably clearing the singleton cache per test, state (auth_file paths, env-loaded tokens, tenant id) can leak between tests and make behavior depend on execution order. The current fixture attempts (__wrapped__, singleton.__wrapped__) don't match how the singleton decorator is implemented, so they won't actually reset anything.
@pytest.fixture
def auth_instance(temp_dir_fixture):
    """Get a fresh FabAuth instance."""
    # Clear singleton for test isolation
    FabAuth.__wrapped__ = None  # type: ignore
    from fabric_cli.core import fab_auth as fab_auth_module

    if FabAuth in fab_auth_module.singleton.__wrapped__:  # type: ignore
        del fab_auth_module.singleton.__wrapped__[FabAuth]  # type: ignore
    return FabAuth()

src/fabric_cli/commands/auth/fab_auth.py:39

  • fab auth login --azure-cli currently only acquires the Fabric token, while other login flows (interactive, SPN, managed identity) also acquire OneLake and Azure management tokens. This makes Azure CLI login behave differently and can leave later commands without the required secondary tokens.
    if getattr(args, "azure_cli", False):
        FabAuth().set_access_mode("azure_cli", args.tenant)
        FabAuth().set_azure_cli(args.tenant)
        FabAuth().get_access_token(scope=fab_constant.SCOPE_FABRIC_DEFAULT)
        Context().context = FabAuth().get_tenant()

src/fabric_cli/core/fab_auth.py:500

  • The Azure CLI token acquisition block includes lines that exceed the repo's Black line-length (88), which will cause formatting churn and makes the code harder to read (e.g., the inline conditional credential construction). Please wrap these statements in Black-friendly form.
        try:
            credential = AzureCliCredential(tenant_id=stored_tenant) if stored_tenant else AzureCliCredential()
            # AzureCliCredential.get_token expects scopes as positional args
            azure_token = credential.get_token(scope[0])
            token_result = {

src/fabric_cli/core/fab_auth.py:522

  • This sanitization fallback message is on a single very long line (over Black's 88-char limit). Wrapping it will keep formatting stable and improve readability.
            error_msg = str(e)
            if any(p.lower() in error_msg.lower() for p in self._SENSITIVE_PATTERNS):
                error_msg = "Azure CLI token acquisition failed. Run 'az account get-access-token' manually to diagnose."
            raise FabricCLIError(
                f"Azure CLI authentication failed: {error_msg}",

All auth modes validate Fabric, OneLake, and Azure scopes at login.
In-memory caching ensures no redundant subprocess calls at runtime.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 11, 2026 11:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

src/fabric_cli/core/fab_auth.py:501

  • _acquire_token_from_azure_cli() calls scope[0] unconditionally (including in credential.get_token(scope[0])). If a caller passes an empty scope list, this will raise IndexError instead of a structured FabricCLIError. Also, AzureCliCredential.get_token expects scopes as positional args; using only scope[0] silently drops additional scopes if they’re ever introduced.
        try:
            credential = AzureCliCredential(tenant_id=stored_tenant) if stored_tenant else AzureCliCredential()
            # AzureCliCredential.get_token expects scopes as positional args
            azure_token = credential.get_token(scope[0])
            token_result = {

tests/test_core/test_fab_auth_azure_cli.py:36

  • The auth_instance fixture is unused and its singleton-reset logic is incorrect for FabAuth (FabAuth is a function returned by the singleton decorator, and singleton() doesn’t expose a wrapped instances map). Keeping this dead code is misleading and risks future test failures if someone starts using it.
@pytest.fixture
def auth_instance(temp_dir_fixture):
    """Get a fresh FabAuth instance."""
    # Clear singleton for test isolation
    FabAuth.__wrapped__ = None  # type: ignore
    from fabric_cli.core import fab_auth as fab_auth_module

    if FabAuth in fab_auth_module.singleton.__wrapped__:  # type: ignore
        del fab_auth_module.singleton.__wrapped__[FabAuth]  # type: ignore
    return FabAuth()

tests/test_core/test_fab_auth_azure_cli.py:56

  • The fresh_auth fixture is unused and attempts to mutate FabAuth.init.globals / singleton internals, which is brittle and not a valid way to reset the singleton. This should be removed to keep the test module deterministic and maintainable.
@pytest.fixture
def fresh_auth(temp_dir_fixture, monkeypatch):
    """Get a fresh FabAuth instance with singleton cleared."""
    # Reset singleton instances dict
    import fabric_cli.core.fab_auth as auth_module

    # Access the closure variable of the singleton decorator
    singleton_instances = auth_module.singleton.__code__.co_consts  # noqa
    # Simpler approach: just patch the module-level reference
    monkeypatch.setattr(
        "fabric_cli.core.fab_auth.FabAuth.__init__.__globals__",
        {},
        raising=False,
    )
    # Re-instantiate
    auth = FabAuth.__new__(FabAuth)
    auth.__init__()
    return auth

Comment thread src/fabric_cli/core/fab_auth.py Outdated
Ensures both --azure-cli flag and interactive menu selection
show the same confirmation message with tenant ID.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 11, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

src/fabric_cli/core/fab_auth.py:477

  • New FabricCLIError messages here are hardcoded strings. Elsewhere in this module, auth errors consistently use ErrorMessages.Auth.* helpers (e.g., invalid_identity_type(), token_acquisition_failed(), access_token_error()), which centralizes wording and keeps UX consistent. Consider adding Azure CLI-specific ErrorMessages.Auth helpers and using them here (and for the other Azure CLI error branches) instead of inline strings.
            raise FabricCLIError(
                "Azure CLI auth requires the 'azure-identity' package. "
                "Install it with: pip install azure-identity",
                status_code=con.ERROR_AUTHENTICATION_FAILED,
            )

src/fabric_cli/core/fab_auth.py:441

  • In set_azure_cli(), identity_type is set before calling set_tenant(). If set_tenant() detects a tenant change it calls logout(), which clears _auth_info and can wipe out the just-set identity_type. This can leave the auth config without identity_type when switching tenants via set_azure_cli(tenant_id=...). Reorder so tenant changes (and any logout) happen first, then set identity_type last.
        self._set_auth_properties(
            {
                con.IDENTITY_TYPE: "azure_cli",
            }
        )

tests/test_core/test_fab_auth_azure_cli.py:57

  • The auth_instance and fresh_auth fixtures are defined but never used in this test module, and they contain brittle/incorrect attempts to reset the @singleton-decorated FabAuth (e.g., mutating FabAuth.wrapped and FabAuth.init.globals). Keeping these unused fixtures risks future accidental use and makes the tests harder to understand; remove them or refactor to a single, actually-used fixture.
@pytest.fixture
def auth_instance(temp_dir_fixture):
    """Get a fresh FabAuth instance."""
    # Clear singleton for test isolation
    FabAuth.__wrapped__ = None  # type: ignore
    from fabric_cli.core import fab_auth as fab_auth_module

    if FabAuth in fab_auth_module.singleton.__wrapped__:  # type: ignore
        del fab_auth_module.singleton.__wrapped__[FabAuth]  # type: ignore
    return FabAuth()


@pytest.fixture
def fresh_auth(temp_dir_fixture, monkeypatch):
    """Get a fresh FabAuth instance with singleton cleared."""

Comment thread src/fabric_cli/core/fab_auth.py Outdated
Comment thread src/fabric_cli/core/fab_auth.py Outdated
Copilot AI review requested due to automatic review settings August 11, 2026 12:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (5)

tests/test_core/test_fab_auth_azure_cli.py:57

  • The fresh_auth fixture contains fragile/incorrect singleton-reset logic (e.g., reading singleton.__code__.co_consts and patching FabAuth.__init__.__globals__ to {}), and it is unused in this test module. Leaving this in place makes the tests harder to understand and could break badly if someone starts using it later.

Consider removing it and relying on a single, correct autouse singleton-reset fixture instead.

@pytest.fixture
def fresh_auth(temp_dir_fixture, monkeypatch):
    """Get a fresh FabAuth instance with singleton cleared."""
    # Reset singleton instances dict
    import fabric_cli.core.fab_auth as auth_module

    # Access the closure variable of the singleton decorator
    singleton_instances = auth_module.singleton.__code__.co_consts  # noqa
    # Simpler approach: just patch the module-level reference
    monkeypatch.setattr(
        "fabric_cli.core.fab_auth.FabAuth.__init__.__globals__",
        {},
        raising=False,
    )
    # Re-instantiate
    auth = FabAuth.__new__(FabAuth)
    auth.__init__()
    return auth

src/fabric_cli/core/fab_auth.py:477

  • This new ImportError path raises FabricCLIError with a hardcoded message. In this codebase, auth errors are consistently sourced from ErrorMessages.Auth.* (see e.g. fab_auth.py:590-636) so messages stay centralized and reusable.

Please add an AuthErrors.azure_cli_missing_dependency() (or similar) and use it here for consistency.

        try:
            from azure.identity import AzureCliCredential, CredentialUnavailableError
        except ImportError:
            raise FabricCLIError(
                "Azure CLI auth requires the 'azure-identity' package. "
                "Install it with: pip install azure-identity",
                status_code=con.ERROR_AUTHENTICATION_FAILED,
            )

src/fabric_cli/core/fab_auth.py:513

  • The Azure CLI unavailable case uses a hardcoded user-facing message. Elsewhere in this module, user-facing auth errors come from ErrorMessages.Auth.*.

Consider adding a dedicated AuthErrors.azure_cli_unavailable() (and possibly a separate one for "not logged in") and using it here so error messaging stays consistent and maintainable.

        except CredentialUnavailableError:
            raise FabricCLIError(
                "Azure CLI is not installed or not logged in. "
                "Run 'az login' to authenticate, then retry.",
                status_code=con.ERROR_AUTHENTICATION_FAILED,
            )

src/fabric_cli/core/fab_auth.py:500

  • This credential = ... if ... else ... line exceeds Black’s default line length and will likely fail formatting checks in CI.

Wrap it onto multiple lines so black src/ tests/ stays clean.

        try:
            credential = AzureCliCredential(tenant_id=stored_tenant) if stored_tenant else AzureCliCredential()
            # AzureCliCredential.get_token expects scopes as positional args
            azure_token = credential.get_token(scope[0])
            token_result = {

tests/test_core/test_fab_auth_azure_cli.py:37

  • The auth_instance fixture tries to reset the FabAuth singleton via __wrapped__, but FabAuth is a custom @singleton wrapper (a closure) and neither FabAuth.__wrapped__ nor fabric_cli.core.fab_auth.singleton.__wrapped__ exist. If this fixture is ever used it will raise AttributeError, and the current tests also risk leaking singleton state between test modules.

Use a robust singleton reset that clears the wrapped closure’s instances dict, ideally as an autouse fixture so all tests in this module get isolation.

This issue also appears on line 39 of the same file.

@pytest.fixture
def auth_instance(temp_dir_fixture):
    """Get a fresh FabAuth instance."""
    # Clear singleton for test isolation
    FabAuth.__wrapped__ = None  # type: ignore
    from fabric_cli.core import fab_auth as fab_auth_module

    if FabAuth in fab_auth_module.singleton.__wrapped__:  # type: ignore
        del fab_auth_module.singleton.__wrapped__[FabAuth]  # type: ignore
    return FabAuth()

…ss calls

During login, _get_azure_cli_tenant() was called 4 times (auto-capture + 3 drift checks). Now caches for 30s, reducing to 1 subprocess call.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 12, 2026 13:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/fabric_cli/core/fab_auth.py:543

  • _acquire_token_from_azure_cli determines whether to surface an SDK error message by comparing type(e).__name__ strings, which is brittle (subclasses/renames won’t match) and can unintentionally hide useful Azure SDK diagnostics. Prefer catching/recognizing the concrete Azure exception types via isinstance.
        except Exception as e:
            # Allowlist: SDK exceptions are pre-sanitized by azure-identity; unknown exceptions get a safe generic message
            if type(e).__name__ in ("ClientAuthenticationError", "HttpResponseError"):
                error_msg = str(e)
            else:

tests/test_parsers/test_fab_auth_parser.py:6

  • Unused import argparse in this test module.
import argparse

tests/test_core/test_fab_auth_azure_cli.py:26

  • This monkeypatch.setattr(...) call has inconsistent indentation that should be normalized (e.g., by running Black) to keep formatting consistent across the test suite.
    # Ensure shutil.which("az") resolves in tests (Windows uses az.cmd)
    monkeypatch.setattr(
         "shutil.which",
         lambda cmd: f"/usr/bin/{cmd}" if cmd == "az" else None,
     )

docs/examples/auth_examples.md:94

  • The interactive-mode example prompt text doesn’t match the actual menu option string in fab_auth.init ("Azure CLI (existing 'az login' session)"). Keeping them identical helps users follow the docs verbatim.
? How would you like to authenticate Fabric CLI? Azure CLI authentication

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 12, 2026 13:46
@shirasassoon
shirasassoon force-pushed the feature/azure-cli-auth-poc branch from 7ec4ee1 to 2a6ffe1 Compare August 12, 2026 13:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (4)

tests/test_core/test_fab_auth_azure_cli.py:26

  • The Azure CLI tenant-discovery fixture has inconsistent indentation that will be reformatted by black (and may fail formatting checks as-is).
    monkeypatch.setattr(
         "shutil.which",
         lambda cmd: f"/usr/bin/{cmd}" if cmd == "az" else None,
     )

docs/examples/auth_examples.md:95

  • The interactive example output doesn't match the actual Azure CLI option string shown in the login menu (currently "Azure CLI (existing 'az login' session)"). Aligning the example avoids confusing copy/paste or screenshot comparisons.
fab auth login
? How would you like to authenticate Fabric CLI? Azure CLI authentication
**src/fabric_cli/core/fab_auth.py:521**
* _acquire_token_from_azure_cli() can raise an IndexError when called with an empty scope list because it later does credential.get_token(scope[0]). Even though callers usually provide a scope, this method is exposed via acquire_token(), so it should fail fast with a structured FabricCLIError when scope is missing/empty, and reuse the computed cache_key when calling get_token.
    cache_key = scope[0] if scope else ""
    cached = self._get_cached_azure_cli_token(cache_key)
    if cached:
        return cached
**tests/test_core/test_fab_msal_bridge_azure_cli.py:30**
* This fixture sets config_location() to tmp_path, but if FabAuth() was already instantiated earlier in the test session (singleton), auth.auth_file/cache_file will still point at the original location. Since set_access_mode() may call logout() and write/remove those paths, this can cause tests to touch real user config instead of tmp_path. Patch auth_file/cache_file (and app) here the same way the other Azure CLI auth tests do.
auth = FabAuth()
auth._azure_cli_token_cache.clear()
auth._cached_az_tenant = None
auth._cached_az_tenant_time = 0.0
auth._auth_info = {}
</details>

Copilot AI review requested due to automatic review settings August 12, 2026 13:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/fabric_cli/core/fab_auth.py:522

  • _acquire_token_from_azure_cli() can still raise IndexError when scope is empty because credential.get_token(scope[0]) is called unconditionally, even though cache_key is guarded. This makes the method inconsistent and can crash token acquisition with an unhandled exception.
        # Check in-memory cache first
        cache_key = scope[0] if scope else ""
        cached = self._get_cached_azure_cli_token(cache_key)
        if cached:
            return cached

        try:
            credential = (
                AzureCliCredential(tenant_id=stored_tenant)
                if stored_tenant
                else AzureCliCredential()
            )
            # AzureCliCredential.get_token expects scopes as positional args
            azure_token = credential.get_token(scope[0])
            token_result = {

src/fabric_cli/core/fab_auth.py:543

  • The exception allowlist uses type(e).name string matching and a broad except Exception as e, which is brittle and could accidentally surface unexpected exception messages. Prefer catching azure-core exceptions explicitly (ClientAuthenticationError/HttpResponseError) and using a separate generic fallback that never includes the raw exception text.
        except CredentialUnavailableError:
            raise FabricCLIError(
                ErrorMessages.Auth.azure_cli_not_available(),
                status_code=con.ERROR_AUTHENTICATION_FAILED,
            )
        except Exception as e:
            # Allowlist: SDK exceptions are pre-sanitized by azure-identity; unknown exceptions get a safe generic message
            if type(e).__name__ in ("ClientAuthenticationError", "HttpResponseError"):
                error_msg = str(e)
            else:
                error_msg = ErrorMessages.Auth.azure_cli_token_acquisition_failed()
            raise FabricCLIError(
                ErrorMessages.Auth.azure_cli_auth_failed(error_msg),
                status_code=con.ERROR_AUTHENTICATION_FAILED,
            )

tests/test_core/test_fab_auth_azure_cli.py:26

  • The monkeypatch.setattr call is mis-indented, which is likely to fail formatting/lint checks (and is inconsistent with the rest of the file’s indentation).
    monkeypatch.setattr(
         "shutil.which",
         lambda cmd: f"/usr/bin/{cmd}" if cmd == "az" else None,
     )

docs/examples/auth_examples.md:95

  • The interactive-mode example doesn’t match the actual menu option string returned by the CLI ("Azure CLI (existing 'az login' session)"). This makes the docs harder to follow and can confuse users trying to verify they selected the right option.

fab auth login
? How would you like to authenticate Fabric CLI? Azure CLI authentication

Copilot AI review requested due to automatic review settings August 12, 2026 14:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/fabric_cli/core/fab_auth.py:528

  • _acquire_token_from_azure_cli() indexes scope[0] when calling AzureCliCredential.get_token(...). If acquire_token() is invoked with an empty scope list (e.g., via TokenCredential.get_token() being called with no scopes), this will raise IndexError instead of a structured FabricCLIError.
        cache_key = scope[0] if scope else ""
        cached = self._get_cached_azure_cli_token(cache_key)
        if cached:
            return cached

src/fabric_cli/core/fab_auth.py:543

  • The Azure CLI auth error sanitization relies on checking exception class names (type(e).name). This is brittle and can unintentionally treat non-SDK exceptions as safe. Prefer isinstance checks against azure.core.exceptions types and keep the original exception chained for debugging.
        except Exception as e:
            # Allowlist: SDK exceptions are pre-sanitized by azure-identity; unknown exceptions get a safe generic message
            if type(e).__name__ in ("ClientAuthenticationError", "HttpResponseError"):
                error_msg = str(e)
            else:

tests/test_core/test_fab_auth_azure_cli.py:26

  • This block is not Black-formatted due to inconsistent indentation in the monkeypatch.setattr(...) call. Since CI runs tox -e lint with Black, this will fail formatting checks.
    monkeypatch.setattr(
         "shutil.which",
         lambda cmd: f"/usr/bin/{cmd}" if cmd == "az" else None,
     )

docs/examples/auth_examples.md:38

  • The interactive login example doesn't match the actual menu option string used by the CLI ("Azure CLI (existing 'az login' session)"). This makes the docs harder to follow when users compare to the prompt output.
? How would you like to authenticate Fabric CLI? Azure CLI authentication

Copilot AI review requested due to automatic review settings August 12, 2026 14:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

tests/test_core/test_fab_auth_azure_cli.py:26

  • Indentation in the monkeypatch.setattr(...) block is inconsistent and likely to fail black formatting checks for the tests suite.
    monkeypatch.setattr(
         "shutil.which",
         lambda cmd: f"/usr/bin/{cmd}" if cmd == "az" else None,
     )

tests/test_parsers/test_fab_auth_parser.py:9

  • argparse is imported but unused in this test module, which can trip linting and adds noise.
import argparse

from fabric_cli.core.fab_parser_setup import CustomArgumentParser
from fabric_cli.parsers import fab_auth_parser

Comment thread src/fabric_cli/core/fab_auth.py
…iceResponseError

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

tests/test_core/test_fab_auth_azure_cli.py:26

  • The monkeypatch.setattr block is over-indented, which will fail Black formatting and makes the test harder to read. Align the arguments with the opening parenthesis indentation used elsewhere in the repo.
    # Ensure shutil.which("az") resolves in tests (Windows uses az.cmd)
    monkeypatch.setattr(
         "shutil.which",
         lambda cmd: f"/usr/bin/{cmd}" if cmd == "az" else None,
     )

docs/commands/auth/index.md:55

  • The docs list the -u flag as --user, but the actual parser defines --username. This mismatch will confuse users following the command reference.
- `-u, --user`: Client ID for service principal. Optional.
- `-p, --password`: Client secret for service principal. Optional.
- `--federated-token`: Federated token for workload identity. Optional.
- `--certificate`: Path to certificate file. Optional.
- `--azure-cli`: Use an existing Azure CLI login session as the token provider. Requires Azure CLI to be installed and logged in (`az login`). Optional.

Shira Sassoon and others added 2 commits August 13, 2026 10:20
…ntial

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (3)

tests/test_core/test_fab_auth_azure_cli.py:26

  • The monkeypatch setattr block has inconsistent indentation; running black would reformat this file and may cause formatting-check CI failures if the project enforces black --check.
    monkeypatch.setattr(
         "shutil.which",
         lambda cmd: f"/usr/bin/{cmd}" if cmd == "az" else None,
     )

tests/test_parsers/test_fab_auth_parser.py:9

  • Unused import: argparse is imported but never referenced in this test module, which adds noise and can trigger unused-import linting in some environments.
import argparse

from fabric_cli.core.fab_parser_setup import CustomArgumentParser
from fabric_cli.parsers import fab_auth_parser

docs/commands/auth/index.md:56

  • The parameter list still documents the client-id flag as -u, --user, but the parser actually defines -u, --username. This mismatch will confuse users following the command reference.
- `--azure-cli`: Use an existing Azure CLI login session as the token provider. Requires Azure CLI to be installed and logged in (`az login`). Optional.
- `--tenant`: Tenant ID. Optional. When used with `--azure-cli`, pins Fabric CLI to the specified tenant.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

tests/test_core/test_fab_auth_azure_cli.py:26

  • This monkeypatch.setattr(...) block has inconsistent indentation compared to other tests (e.g. tests/test_core/test_context_persistence.py:96-98) and is not Black-formatted, which can cause style checks to fail.
    monkeypatch.setattr(
         "shutil.which",
         lambda cmd: f"/usr/bin/{cmd}" if cmd == "az" else None,
     )

tests/test_parsers/test_fab_auth_parser.py:7

  • Unused import: argparse is not referenced anywhere in this test file; it should be removed to avoid lint failures and keep imports minimal.
import argparse

…voke MSAL

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

docs/commands/auth/index.md:55

  • Docs list the login flag as -u, --user, but the CLI parser defines -u, --username. This makes the command reference misleading for users trying to use service principal or managed identity client IDs.
- `-u, --user`: Client ID for service principal. Optional.
- `-p, --password`: Client secret for service principal. Optional.
- `--federated-token`: Federated token for workload identity. Optional.
- `--certificate`: Path to certificate file. Optional.
- `--azure-cli`: Use an existing Azure CLI login session as the token provider. Requires Azure CLI to be installed and logged in (`az login`). Optional.

tests/test_core/test_fab_auth_azure_cli.py:26

  • The monkeypatch.setattr("shutil.which", ...) block is not Black-formatted (extra indentation on the wrapped arguments). CI enforces Black via pre-commit (see .github/instructions/test.instructions.md:51), so this may fail formatting checks.
    monkeypatch.setattr(
         "shutil.which",
         lambda cmd: f"/usr/bin/{cmd}" if cmd == "az" else None,
     )

…ic-cicd)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (5)

tests/test_core/test_fab_auth_azure_cli.py:26

  • The monkeypatch.setattr call has inconsistent indentation (extra leading spaces) which is likely to be reformatted by black / fail formatting checks.
    monkeypatch.setattr(
         "shutil.which",
         lambda cmd: f"/usr/bin/{cmd}" if cmd == "az" else None,
     )

tests/test_parsers/test_fab_auth_parser.py:6

  • argparse is imported but never used in this test module; this is dead code and can fail linting.
import argparse

src/fabric_cli/core/fab_auth.py:513

  • _acquire_token_from_azure_cli unconditionally uses scope[0] later (for both caching and credential.get_token(scope[0])). If this method is ever called with an empty scope list (e.g., via MsalTokenCredential.get_token() invoked with no scopes), it will raise IndexError instead of a structured FabricCLIError.
        cache_key = scope[0] if scope else ""
        cached = self._get_cached_azure_cli_token(cache_key)
        if cached:
            return cached

pyproject.toml:23

  • PR description says the dependency change is azure-identity>=1.15.0, but pyproject.toml sets azure-identity>=1.25.0. Please reconcile the minimum supported version (either update the PR description or adjust the constraint).
    "azure-identity>=1.25.0",

docs/commands/auth/index.md:56

  • The parameters list documents the client-id flag as --user (line 51), but the actual CLI flag is --username (see src/fabric_cli/parsers/fab_auth_parser.py). This will mislead users copying the usage docs.
- `--azure-cli`: Use an existing Azure CLI login session as the token provider. Requires Azure CLI to be installed and logged in (`az login`). Optional.
- `--tenant`: Tenant ID. Optional. When used with `--azure-cli`, pins Fabric CLI to the specified tenant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Support Azure Cli credential

2 participants